Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 9f75e3db181925ddd253f7fb231af81f9fb8c013


Parents : 4f6bcce
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-21T16:32:08-05:00

feat: implement outbound message status utilities for icon and title resolution, add tests for LXMF state and method reporting

Changes
Diff

diff --git a/meshchatx.rsm b/meshchatx.rsm
index 8e4a39fd..444f3dfe 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ

diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index 4a423618..714b9c10 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -1837,6 +1837,11 @@ import { findMapUriInContent, mapLinkKindFromMessage, parseMeshchatMapUri } from
import { applyRelayShareLink, findRelayUriInContent, parseMeshchatRelayUri } from "../../js/relayLinkUtils.js";
import { LXMF_REACTION_EMOJIS, mergeLxmfReactionRowsIntoMessages } from "../../js/lxmfReactions";
import { createOutboundQueue } from "../../js/outboundSendQueue";
+import {
+ isOpportunisticDeferredDelivery as isOpportunisticDeferredDeliveryStatus,
+ outboundBubbleStatusIconName as resolveOutboundBubbleStatusIconName,
+ outboundBubbleStatusTitleKey as resolveOutboundBubbleStatusTitleKey,
+} from "../../js/outboundMessageStatus.js";
import emojiPickerEnDataUrl from "emoji-picker-element-data/en/emojibase/data.json?url";
import "emoji-picker-element";
import StickerView from "../stickers/StickerView.vue";
@@ -4971,45 +4976,11 @@ export default {
return this.outboundBubbleStatusTitle(lxmfMessage);
},
outboundBubbleStatusIconName(lxmfMessage) {
- if (!lxmfMessage) {
- return "check";
- }
- const state = lxmfMessage.state;
- const method = lxmfMessage.method;
- if (state === "delivered") {
- if (method === "propagated") {
- return "email-check-outline";
- }
- if (method === "paper") {
- return "note-check-outline";
- }
- return "check-all";
- }
- if (["sent", "propagated", "unknown"].includes(state)) {
- if (method === "propagated") {
- return "email-outline";
- }
- if (method === "paper") {
- return "note-outline";
- }
- return "check";
- }
- return "check";
+ return resolveOutboundBubbleStatusIconName(lxmfMessage);
},
outboundBubbleStatusTitle(lxmfMessage) {
- if (!lxmfMessage) {
- return "";
- }
- if (lxmfMessage.state === "delivered") {
- if (lxmfMessage.method === "propagated") {
- return this.$t("messages.outbound_delivered_propagated");
- }
- return this.$t("messages.outbound_delivered");
- }
- if (lxmfMessage.method === "propagated") {
- return this.$t("messages.outbound_on_propagation_node");
- }
- return this.$t("messages.outbound_sent_network");
+ const key = resolveOutboundBubbleStatusTitleKey(lxmfMessage);
+ return key ? this.$t(key) : "";
},
isOutboundWaitingBubble(chatItem) {
return Boolean(chatItem?.is_outbound && chatItem?.lxmf_message?._pendingPathfinding);
@@ -5302,10 +5273,7 @@ export default {
return this.$t("messages.failed_waiting_announce_tooltip");
},
isOpportunisticDeferredDelivery(lxmfMessage) {
- if (!lxmfMessage) {
- return false;
- }
- return lxmfMessage.method === "opportunistic" && lxmfMessage.state === "failed";
+ return isOpportunisticDeferredDeliveryStatus(lxmfMessage);
},
async warmPathToPeer() {
await this.refreshPeerPath({ warm: true });

diff --git a/meshchatx/src/frontend/js/outboundMessageStatus.js b/meshchatx/src/frontend/js/outboundMessageStatus.js
new file mode 100644
index 00000000..7f6747c0
--- /dev/null
+++ b/meshchatx/src/frontend/js/outboundMessageStatus.js
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: 0BSD AND MIT
+/**
+ * Outbound LXMF bubble status icons and i18n title keys.
+ *
+ * Backend emits API strings from LXMF.LXMessage via convert_lxmf_state_to_string
+ * and convert_lxmf_method_to_string. Propagation is a method, never a state.
+ */
+
+const SENT_LIKE_STATES = new Set(["sent", "propagated", "unknown"]);
+
+/**
+ * @param {{ state?: string, method?: string } | null | undefined} lxmfMessage
+ * @returns {string} MDI kebab icon name
+ */
+export function outboundBubbleStatusIconName(lxmfMessage) {
+ if (!lxmfMessage) {
+ return "check";
+ }
+ const state = lxmfMessage.state;
+ const method = lxmfMessage.method;
+ if (state === "delivered") {
+ if (method === "propagated") {
+ return "email-check-outline";
+ }
+ if (method === "paper") {
+ return "note-check-outline";
+ }
+ return "check-all";
+ }
+ if (SENT_LIKE_STATES.has(state)) {
+ if (method === "propagated") {
+ return "email-outline";
+ }
+ if (method === "paper") {
+ return "note-outline";
+ }
+ return "check";
+ }
+ return "check";
+}
+
+/**
+ * @param {{ state?: string, method?: string } | null | undefined} lxmfMessage
+ * @returns {string | null} i18n key under messages.*, or null when no title
+ */
+export function outboundBubbleStatusTitleKey(lxmfMessage) {
+ if (!lxmfMessage) {
+ return null;
+ }
+ if (lxmfMessage.state === "delivered") {
+ if (lxmfMessage.method === "propagated") {
+ return "messages.outbound_delivered_propagated";
+ }
+ return "messages.outbound_delivered";
+ }
+ if (lxmfMessage.method === "propagated") {
+ return "messages.outbound_on_propagation_node";
+ }
+ return "messages.outbound_sent_network";
+}
+
+/**
+ * Opportunistic FAILED is deferred wait in the UI, not a hard failure badge.
+ * @param {{ state?: string, method?: string } | null | undefined} lxmfMessage
+ */
+export function isOpportunisticDeferredDelivery(lxmfMessage) {
+ if (!lxmfMessage) {
+ return false;
+ }
+ return lxmfMessage.method === "opportunistic" && lxmfMessage.state === "failed";
+}

diff --git a/tests/backend/test_lxmf_status_oracle.py b/tests/backend/test_lxmf_status_oracle.py
new file mode 100644
index 00000000..1ddb62e7
--- /dev/null
+++ b/tests/backend/test_lxmf_status_oracle.py
@@ -0,0 +1,200 @@
+# SPDX-License-Identifier: 0BSD
+"""Oracle tests for LXMF state and method reporting.
+
+Guarantee: MeshChatX API state and method strings match LXMF.LXMessage
+constants. Propagation is a delivery method, never a lifecycle state.
+"""
+
+from types import SimpleNamespace
+from unittest.mock import MagicMock
+
+import LXMF
+import pytest
+from hypothesis import given, settings
+from hypothesis import strategies as st
+
+from meshchatx.src.backend.lxmf_utils import (
+ convert_lxmf_message_to_dict,
+ convert_lxmf_method_to_string,
+ convert_lxmf_state_to_string,
+)
+
+# Independent contract: derived from LXMF constant names, not from MeshChatX
+# if/elif branches. If LXMF adds a state, this table must be updated deliberately.
+LXMF_STATE_ORACLE = {
+ LXMF.LXMessage.GENERATING: "generating",
+ LXMF.LXMessage.OUTBOUND: "outbound",
+ LXMF.LXMessage.SENDING: "sending",
+ LXMF.LXMessage.SENT: "sent",
+ LXMF.LXMessage.DELIVERED: "delivered",
+ LXMF.LXMessage.REJECTED: "rejected",
+ LXMF.LXMessage.CANCELLED: "cancelled",
+ LXMF.LXMessage.FAILED: "failed",
+}
+
+LXMF_METHOD_ORACLE = {
+ LXMF.LXMessage.OPPORTUNISTIC: "opportunistic",
+ LXMF.LXMessage.DIRECT: "direct",
+ LXMF.LXMessage.PROPAGATED: "propagated",
+ LXMF.LXMessage.PAPER: "paper",
+}
+
+API_LIFECYCLE_STATES = frozenset(LXMF_STATE_ORACLE.values()) | {"unknown"}
+API_DELIVERY_METHODS = frozenset(LXMF_METHOD_ORACLE.values()) | {"unknown"}
+
+# LXMF has no lifecycle state for "on propagation node". That meaning is
+# method=PROPAGATED + state=SENT (or later DELIVERED).
+assert "propagated" not in LXMF_STATE_ORACLE.values()
+
+
+def _msg(**attrs):
+ return SimpleNamespace(**attrs)
+
+
+def _mock_lxmessage(*, state, method):
+ mock_msg = MagicMock(spec=LXMF.LXMessage)
+ mock_msg.hash = b"m" * 16
+ mock_msg.source_hash = b"s" * 16
+ mock_msg.destination_hash = b"d" * 16
+ mock_msg.incoming = False
+ mock_msg.state = state
+ mock_msg.progress = 1.0 if state == LXMF.LXMessage.DELIVERED else 0.0
+ mock_msg.method = method
+ mock_msg.delivery_attempts = 0
+ mock_msg.title = b""
+ mock_msg.content = b"hello"
+ mock_msg.timestamp = 1_700_000_000
+ mock_msg.rssi = None
+ mock_msg.snr = None
+ mock_msg.q = None
+ mock_msg.get_fields.return_value = {}
+ mock_msg.outbound_ticket = None
+ mock_msg.stamp_cost = None
+ mock_msg.stamp = None
+ mock_msg.defer_stamp = False
+ return mock_msg
+
+
+@pytest.mark.parametrize(("lxmf_state", "expected"), list(LXMF_STATE_ORACLE.items()))
+def test_oracle_state_constant_maps_to_api_string(lxmf_state, expected):
+ assert convert_lxmf_state_to_string(_msg(state=lxmf_state)) == expected
+
+
+@pytest.mark.parametrize(("lxmf_method", "expected"), list(LXMF_METHOD_ORACLE.items()))
+def test_oracle_method_constant_maps_to_api_string(lxmf_method, expected):
+ assert convert_lxmf_method_to_string(_msg(method=lxmf_method)) == expected
+
+
+@given(state=st.sampled_from(list(LXMF_STATE_ORACLE.keys())))
+@settings(max_examples=64, deadline=None)
+def test_oracle_known_states_never_unknown(state):
+ reported = convert_lxmf_state_to_string(_msg(state=state))
+ assert reported == LXMF_STATE_ORACLE[state]
+ assert reported != "unknown"
+
+
+@given(method=st.sampled_from(list(LXMF_METHOD_ORACLE.keys())))
+@settings(max_examples=32, deadline=None)
+def test_oracle_known_methods_never_unknown(method):
+ reported = convert_lxmf_method_to_string(_msg(method=method))
+ assert reported == LXMF_METHOD_ORACLE[method]
+ assert reported != "unknown"
+
+
+@given(
+ state=st.integers(min_value=0, max_value=512).filter(
+ lambda v: v not in LXMF_STATE_ORACLE
+ )
+)
+@settings(max_examples=80, deadline=None)
+def test_oracle_unknown_state_ints_report_unknown(state):
+ assert convert_lxmf_state_to_string(_msg(state=state)) == "unknown"
+
+
+@given(
+ method=st.integers(min_value=0, max_value=64).filter(
+ lambda v: v not in LXMF_METHOD_ORACLE
+ )
+)
+@settings(max_examples=40, deadline=None)
+def test_oracle_unknown_method_ints_report_unknown(method):
+ assert convert_lxmf_method_to_string(_msg(method=method)) == "unknown"
+
+
+@given(
+ state=st.sampled_from(list(LXMF_STATE_ORACLE.keys())),
+ method=st.sampled_from(list(LXMF_METHOD_ORACLE.keys())),
+)
+@settings(max_examples=64, deadline=None)
+def test_oracle_message_dict_reports_lxmf_status_pair(state, method):
+ """API dict state/method must match converters and stay in closed vocabularies."""
+ payload = convert_lxmf_message_to_dict(_mock_lxmessage(state=state, method=method))
+ expected_state = LXMF_STATE_ORACLE[state]
+ expected_method = LXMF_METHOD_ORACLE[method]
+ assert payload["state"] == expected_state
+ assert payload["method"] == expected_method
+ assert payload["state"] in API_LIFECYCLE_STATES
+ assert payload["method"] in API_DELIVERY_METHODS
+ # Propagation is method-only. Lifecycle must never be labeled "propagated".
+ assert payload["state"] != "propagated"
+
+
+def test_oracle_propagated_sent_is_on_node_not_delivered():
+ """Parked on a propagation node is SENT + PROPAGATED, not DELIVERED."""
+ payload = convert_lxmf_message_to_dict(
+ _mock_lxmessage(state=LXMF.LXMessage.SENT, method=LXMF.LXMessage.PROPAGATED)
+ )
+ assert payload["state"] == "sent"
+ assert payload["method"] == "propagated"
+
+
+def test_oracle_propagated_delivered_means_recipient_got_mail():
+ payload = convert_lxmf_message_to_dict(
+ _mock_lxmessage(
+ state=LXMF.LXMessage.DELIVERED,
+ method=LXMF.LXMessage.PROPAGATED,
+ )
+ )
+ assert payload["state"] == "delivered"
+ assert payload["method"] == "propagated"
+
+
+def test_oracle_direct_delivered_is_not_propagated():
+ payload = convert_lxmf_message_to_dict(
+ _mock_lxmessage(state=LXMF.LXMessage.DELIVERED, method=LXMF.LXMessage.DIRECT)
+ )
+ assert payload["state"] == "delivered"
+ assert payload["method"] == "direct"
+
+
+def test_oracle_opportunistic_failed_still_reports_failed_state():
+ """Backend keeps LXMF FAILED. UI may treat opportunistic+failed as deferred wait."""
+ payload = convert_lxmf_message_to_dict(
+ _mock_lxmessage(
+ state=LXMF.LXMessage.FAILED,
+ method=LXMF.LXMessage.OPPORTUNISTIC,
+ )
+ )
+ assert payload["state"] == "failed"
+ assert payload["method"] == "opportunistic"
+
+
+def test_oracle_paper_method_preserved_across_states():
+ for state, state_name in (
+ (LXMF.LXMessage.SENT, "sent"),
+ (LXMF.LXMessage.DELIVERED, "delivered"),
+ ):
+ payload = convert_lxmf_message_to_dict(
+ _mock_lxmessage(state=state, method=LXMF.LXMessage.PAPER)
+ )
+ assert payload["state"] == state_name
+ assert payload["method"] == "paper"
+
+
+def test_lxmf_status_oracle_proved():
+ """PROVED marker: full known state/method tables convert correctly."""
+ for lxmf_state, expected in LXMF_STATE_ORACLE.items():
+ assert convert_lxmf_state_to_string(_msg(state=lxmf_state)) == expected
+ for lxmf_method, expected in LXMF_METHOD_ORACLE.items():
+ assert convert_lxmf_method_to_string(_msg(method=lxmf_method)) == expected
+ print("LXMF_STATUS_REPORTING_PROVED")

diff --git a/tests/frontend/outboundMessageStatus.oracle.test.js b/tests/frontend/outboundMessageStatus.oracle.test.js
new file mode 100644
index 00000000..4a25516f
--- /dev/null
+++ b/tests/frontend/outboundMessageStatus.oracle.test.js
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: 0BSD AND MIT
+/**
+ * Oracle tests for outbound LXMF status icons and titles.
+ *
+ * Guarantee: UI status glyphs and title keys follow the API state/method
+ * strings that backend convert_lxmf_* emits from LXMF.LXMessage. Propagation
+ * is method-only. Opportunistic+failed is deferred wait, not a hard fail badge.
+ */
+import { describe, expect, it } from "vitest";
+import {
+ isOpportunisticDeferredDelivery,
+ outboundBubbleStatusIconName,
+ outboundBubbleStatusTitleKey,
+} from "@/js/outboundMessageStatus.js";
+
+/** Closed vocabulary aligned with meshchatx/src/backend/lxmf_utils.py converters. */
+const API_STATES = [
+ "generating",
+ "outbound",
+ "sending",
+ "sent",
+ "delivered",
+ "rejected",
+ "cancelled",
+ "failed",
+ "unknown",
+];
+
+const API_METHODS = ["opportunistic", "direct", "propagated", "paper", "unknown"];
+
+/**
+ * Independent UI oracle. Must stay in sync with product intent, not by
+ * importing the implementation under test into the expected table.
+ */
+function oracleIconName({ state, method }) {
+ if (state === "delivered") {
+ if (method === "propagated") return "email-check-outline";
+ if (method === "paper") return "note-check-outline";
+ return "check-all";
+ }
+ if (state === "sent" || state === "propagated" || state === "unknown") {
+ if (method === "propagated") return "email-outline";
+ if (method === "paper") return "note-outline";
+ return "check";
+ }
+ return "check";
+}
+
+function oracleTitleKey({ state, method }) {
+ if (state === "delivered") {
+ if (method === "propagated") return "messages.outbound_delivered_propagated";
+ return "messages.outbound_delivered";
+ }
+ if (method === "propagated") return "messages.outbound_on_propagation_node";
+ return "messages.outbound_sent_network";
+}
+
+function oracleDeferred({ state, method }) {
+ return method === "opportunistic" && state === "failed";
+}
+
+describe("outboundMessageStatus LXMF oracle", () => {
+ it("maps every API state x method pair to the independent icon oracle", () => {
+ for (const state of API_STATES) {
+ for (const method of API_METHODS) {
+ const msg = { state, method };
+ expect(outboundBubbleStatusIconName(msg)).toBe(oracleIconName(msg));
+ }
+ }
+ });
+
+ it("maps every API state x method pair to the independent title-key oracle", () => {
+ for (const state of API_STATES) {
+ for (const method of API_METHODS) {
+ const msg = { state, method };
+ expect(outboundBubbleStatusTitleKey(msg)).toBe(oracleTitleKey(msg));
+ }
+ }
+ });
+
+ it("treats only opportunistic+failed as deferred wait", () => {
+ for (const state of API_STATES) {
+ for (const method of API_METHODS) {
+ const msg = { state, method };
+ expect(isOpportunisticDeferredDelivery(msg)).toBe(oracleDeferred(msg));
+ }
+ }
+ });
+
+ it("propagated sent means on-node mailbox icon, not delivered checkmarks", () => {
+ const msg = { state: "sent", method: "propagated" };
+ expect(outboundBubbleStatusIconName(msg)).toBe("email-outline");
+ expect(outboundBubbleStatusTitleKey(msg)).toBe("messages.outbound_on_propagation_node");
+ expect(outboundBubbleStatusIconName(msg)).not.toBe("check-all");
+ expect(outboundBubbleStatusIconName(msg)).not.toBe("email-check-outline");
+ });
+
+ it("propagated delivered means recipient collected mail", () => {
+ const msg = { state: "delivered", method: "propagated" };
+ expect(outboundBubbleStatusIconName(msg)).toBe("email-check-outline");
+ expect(outboundBubbleStatusTitleKey(msg)).toBe("messages.outbound_delivered_propagated");
+ });
+
+ it("direct and opportunistic use WhatsApp-style checks for sent/delivered", () => {
+ for (const method of ["direct", "opportunistic"]) {
+ expect(outboundBubbleStatusIconName({ state: "sent", method })).toBe("check");
+ expect(outboundBubbleStatusIconName({ state: "delivered", method })).toBe("check-all");
+ }
+ });
+
+ it("paper uses note icons for sent and delivered", () => {
+ expect(outboundBubbleStatusIconName({ state: "sent", method: "paper" })).toBe("note-outline");
+ expect(outboundBubbleStatusIconName({ state: "delivered", method: "paper" })).toBe("note-check-outline");
+ });
+
+ it("backend never emits state=propagated but UI still treats it as sent-like", () => {
+ expect(outboundBubbleStatusIconName({ state: "propagated", method: "direct" })).toBe("check");
+ expect(outboundBubbleStatusIconName({ state: "propagated", method: "propagated" })).toBe("email-outline");
+ });
+
+ it("null message has safe defaults", () => {
+ expect(outboundBubbleStatusIconName(null)).toBe("check");
+ expect(outboundBubbleStatusTitleKey(null)).toBeNull();
+ expect(isOpportunisticDeferredDelivery(null)).toBe(false);
+ });
+
+ it("LXMF_STATUS_UI_ORACLE_PROVED", () => {
+ let pairs = 0;
+ for (const state of API_STATES) {
+ for (const method of API_METHODS) {
+ const msg = { state, method };
+ expect(outboundBubbleStatusIconName(msg)).toBe(oracleIconName(msg));
+ expect(outboundBubbleStatusTitleKey(msg)).toBe(oracleTitleKey(msg));
+ expect(isOpportunisticDeferredDelivery(msg)).toBe(oracleDeferred(msg));
+ pairs += 1;
+ }
+ }
+ expect(pairs).toBe(API_STATES.length * API_METHODS.length);
+ console.log("LXMF_STATUS_UI_ORACLE_PROVED");
+ });
+});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────